package com.example.commons.redis;

import com.example.commons.exception.BusinessException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import java.util.UUID;

//使用jedis实现redis分布式锁，思路是将线程ID和uuid组合成val放进redis，如果设置成功了，就开启一个守护去给key续期(可能会存在业务还没处理完，但是key过期了，就得给key续期)
//业务处理完后，停止守护线程，判断是否是自己加的锁，如果是就删除key
@Component
public class RedisLock {
    @Autowired
    @Qualifier("jedisPool")
    private JedisPool jedisPool;

    private final int DB = 0;
    private final String LOCK_KEY = "REDIS:LOCK:%s";

    private volatile boolean running = true;

    private Jedis getJedis(int db) {
        Jedis jedis = jedisPool.getResource();
        jedis.select(db);
        return jedis;
    }


    /**
     * @param key         业务key
     * @param expireTime  超时时间/ms
     * @param waitTimeout 等待时间，超过等待时间就放弃/ms
     * @param val         uuid
     *                    键值对的value值，一般为线程ID+uuid，在做删除操作时拿来对比
     *                    <p> SetParams
     *                    ex: 设置键值的过期时间（单位为秒）。
     *                    px: 设置键值的过期时间（单位为毫秒）。
     *                    nx: 仅在键不存在时设置键值。
     *                    xx: 仅在键已经存在时更新键值。  </p>
     * @return boolean true加锁成功 false加锁失败
     */
    public boolean tryLock(String key, String val, long expireTime, long waitTimeout) {
        this.check(expireTime, waitTimeout);
        key = String.format(LOCK_KEY, key);
        val = Thread.currentThread().getId() + "-" + val;
        try (Jedis jedis = this.getJedis(DB)) {
            //计算等待超时时间，超过时间直接返回获取锁失败
            long deadTimeLine = System.currentTimeMillis() + waitTimeout;
            //是否继续获取锁
            boolean flag = true;
            //锁是否获取成功
            boolean result = false;
            while (flag) {
                SetParams setParams = new SetParams();
                setParams.nx();
                setParams.ex(expireTime);
                //成功返回OK，否则返回null
                String res = jedis.set(key, val, setParams);
                if ("OK".equals(res)) {
                    flag = false;
                    result = true;
                    System.out.println(val + "加锁成功，开始守护");
                    this.guardThread(key, expireTime);
                } else {
                    waitTimeout = deadTimeLine - System.currentTimeMillis();
                    // 超过等待时间仍然没有成功获取锁的话就放弃，这里判断大于100是因为下面睡眠了100，如果小于100，直接返回获取锁失败
                    if (waitTimeout > 100L) {
                        try {
                            //防止一直获取锁
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        flag = true;
                    } else {
                        flag = false;
                    }
                    result = false;
                }
            }
            return result;
        }
    }

    /**
     * 看门狗机制，开启守护进程，进行key续期
     *
     * @param key
     * @param expireTime
     */
    private void guardThread(String key, long expireTime) {
        Thread thread = new Thread(() -> {
            System.out.println("进来");

            try {
                //先睡眠一下，防止一进来就续期
                Thread.sleep(expireTime - 500);
            } catch (Exception e) {
                e.printStackTrace();
                return;
            }
            while (this.running) {
                try (Jedis jedis = this.getJedis(DB)) {
                    if (jedis.exists(key)) {
                        //续期
                        jedis.expire(key, expireTime);
                        //查询key是否设置了过期时间 -2为key不存在 -1为key存在但是没有过期时间 是否以秒为单位返回剩余时间，如果为-1再次设置
                        if (-1 == jedis.ttl(key)) {
                            jedis.expire(key, expireTime);
                        }
                        //超过两次设置失败就不守护了
                        if (-1 == jedis.ttl(key)) {
                            this.running = false;
                        } else {
                            try {
                                Thread.sleep(expireTime - 500);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                                this.running = false;
                            }
                        }
                    } else {
                        this.running = false;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    this.running = false;
                }
            }
        });
        //java中线程分为两种类型：用户线程和守护线程。通过Thread.setDaemon(false)设置为用户线程；通过Thread.setDaemon(true)设置为守护线程。如果不设置次属性，默认为用户线程。
        thread.setDaemon(true);
        thread.start();
    }

    private void threadStop() {
        this.running = false;
    }

    public void unlock(String key, String val) {
        this.threadStop();
        key = String.format(LOCK_KEY, key);
        val = Thread.currentThread().getId() + "-" + val;
        try (Jedis jedis = this.getJedis(DB)) {
            String s = jedis.get(key);
            //确保有锁存在并且是自己加的锁
            if (StringUtils.isNotBlank(s) && s.equals(val)) {
                jedis.del(key);
            }
        }
    }


    //检查时间是否规范
    private void check(long expireTime, long waitTimeout) {
        if (0 >= waitTimeout) {
            throw new BusinessException("超时时间有误");
        }
        if (500 >= expireTime) {
            throw new BusinessException("时间应大于500ms");
        }
    }


    public static void main(String[] args) {
        String val = UUID.randomUUID().toString().replace("-", "");
        System.out.println(Thread.currentThread().getId() + "-" + val);
    }
}
